﻿using System;
using System.Collections.Generic;
using System.Text;
using System.IO;

namespace DeflateLib
{
    /// <summary>
    /// Source: tar (file format) - Wikipedia, the free encyclopedia
    /// </summary>
    public static class TarUtils
    {        
        public static TarHeader ReadHeader(Stream s)
        {
            byte[] headerBytes = ReadHeaderBytes(s);
            if (headerBytes == null) return null;

            TarHeader header = new TarHeader();
            header.Filename = GetString(headerBytes, 0, 100);
            header.Filemode = GetOctalNumber(headerBytes, 100, 8);
            header.OwnerID = GetOctalNumber(headerBytes, 108, 8);
            header.GroupID = GetOctalNumber(headerBytes, 116, 8);
            header.Filesize = GetLongOctalNumber(headerBytes, 124,12);
            long? lastModifiedInUnixFormat = GetLongOctalNumber(headerBytes, 136, 12);
            if (lastModifiedInUnixFormat.HasValue)
            {
                header.LastModified = new DateTime(1970, 1, 1, 0,0,0, DateTimeKind.Utc).
                    AddSeconds(lastModifiedInUnixFormat.Value);
            }
            header.LinkIndicator = (char)headerBytes[156];
            header.LinkedFile = GetString(headerBytes, 157, 100);

            string ustarSignature = GetString(headerBytes, 257, 6);
            if (ustarSignature == "ustar")
            {
                header.Owner = GetString(headerBytes, 265, 32);
                header.Group = GetString(headerBytes, 297, 32);
                header.DeviceMajorNumber = GetOctalNumber(headerBytes, 329, 8);
                header.DeviceMinorNumber = GetOctalNumber(headerBytes, 337, 8);
                header.FilenamePrefix = GetString(headerBytes, 345, 155);
                header.IsUstar = true;
            }
            else
                header.IsUstar = false;

            return header;
        }

        const int HeaderSize = 512;

        static byte[] ReadHeaderBytes(Stream s)
        {
            byte[] header = new byte[HeaderSize];
            int read = s.Read(header, 0, HeaderSize);
            if (read != HeaderSize)
                throw new InvalidOperationException(String.Format("Header was not fully read expected {0} found {1}", HeaderSize, read));

            int i = 0;
            while (i < HeaderSize && header[i] == 0) i++;
            if (i == HeaderSize)
                return null; // end of stream found

            // check sum
            int? headerCheckSum = GetOctalNumber(header, 148, 8);
            int checkSum = 8 * 32;
            for (i = 0; i < 148; i++)
            {
                checkSum += header[i];
            }
            for (i = 156; i < 512; i++)
            {
                checkSum += header[i];
            }
            if (checkSum != headerCheckSum.GetValueOrDefault())
                throw new InvalidOperationException("Invalid header checksum");
            return header;
        }

        static long? GetLongOctalNumber(byte[] data, int offset, int count)
        {
            int i = 0;
            while (i < count && (data[offset + i] == 0 || data[offset + i] == ' ')) i++;
            if (i == count) return null;
            long result = 0;
            while (i < count && !(data[offset + i] == 0 || data[offset + i] == ' '))
            {
                byte digit = data[offset + i];
                if (digit < '0' || digit > '7')
                    if (i != count) throw new FormatException("Invalid octal number");
                result = result * 8 + (digit - '0');
                i++;
            }
            while (i < count && (data[offset + i] == 0 || data[offset + i] == ' ')) i++;
            if (i != count) throw new FormatException("Invalid octal number");
            return result;
        }

        static int? GetOctalNumber(byte[] data, int offset, int count)
        {
            int i = 0;
            while (i < count && (data[offset + i] == 0 || data[offset + i] == ' ')) i++;
            if (i == count) return null;
            int result = 0;
            while (i < count && !(data[offset + i] == 0 || data[offset + i] == ' ')) 
            {
                byte digit = data[offset + i];
                if(digit < '0' || digit > '7')
                    if (i != count) throw new FormatException("Invalid octal number");
                result = result * 8 + (digit - '0');
                i++;
            }
            while (i < count && (data[offset + i] == 0 || data[offset + i] == ' ')) i++;
            if (i != count) throw new FormatException("Invalid octal number");
            return result;
        }

        static string GetString(byte[] data, int offset, int count)
        {
            StringBuilder sb = new StringBuilder();
            int i = 0;
            while(i < count && data[offset + i] != 0) i++;

            return Encoding.UTF8.GetString(data, offset, i);
        }
    }

    public class TarHeader
    {
        public string Filename { get; set; }
        public int? Filemode { get; set; }
        public int? OwnerID { get; set; }
        public int? GroupID { get; set; }
        public long? Filesize { get; set; }
        public DateTime? LastModified { get; set; }
        public char LinkIndicator { get; set; }
        public string LinkedFile { get; set; }
        public bool IsUstar { get; set; }
        public string Owner { get; set; }
        public string Group { get; set; }
        public int? DeviceMajorNumber { get; set; }
        public int? DeviceMinorNumber { get; set; }
        public string FilenamePrefix { get; set; }

        public string Fullname
        {
            get { return FilenamePrefix + Filename; }
        }

        public bool IsDirectory
        {
            get { return Filename.EndsWith("/"); }
        }
    }
}
